﻿using System;
using System.CodeDom;
using System.Linq;
using System.Reflection;

namespace GenerateAccessors
{
	static class Code
	{
		/// <summary>
		/// Creates an expression tree for 'this.PrivateObject' where PrivateObject is an instance field,
		/// it's name is given by <see cref="Constants.PrivateObjectField"/>.
		/// </summary>
		/// <returns></returns>
		public static CodeExpression CreatePrivateObjectReferenceExpression()
		{
			return new CodeFieldReferenceExpression(
				new CodeThisReferenceExpression(),
				Constants.PrivateObjectField
			);
		}

		/// <summary>
		/// Creates an expression tree for 'PrivateType' where PrivateType is a static field,
		/// it's name is given by <see cref="Constants.PrivateTypeField"/>.
		/// </summary>
		/// <returns></returns>
		public static CodeExpression CreatePrivateTypeReferenceExpression()
		{
			return new CodeFieldReferenceExpression(
				null,
				Constants.PrivateTypeField
			);
		}

		/// <summary>
		/// Creates an expression tree for 'BindingFlags.NonPublic | BindingFlags.Instance'.
		/// </summary>
		/// <param name="bPublic">Use <see cref="BindingFlags.Public"/> instead of <see cref="BindingFlags.NonPublic"/>.</param>
		/// <param name="bStatic">Use <see cref="BindingFlags.Static"/> instead of <see cref="BindingFlags.Instance"/>.</param>
		/// <returns></returns>
		public static CodeExpression CreateBindingFlagsReferenceExpression(bool bPublic, bool bStatic)
		{
			return new CodeBinaryOperatorExpression(
				new CodeFieldReferenceExpression(
					new CodeTypeReferenceExpression(typeof(BindingFlags)),
					bPublic ? "Public" : "NonPublic"
				),
				CodeBinaryOperatorType.BitwiseOr,
				new CodeFieldReferenceExpression(
					new CodeTypeReferenceExpression(typeof(BindingFlags)),
					bStatic ? "Static" : "Instance"
				)
			);
		}

		/// <summary>
		/// Last resort method to create the <see cref="Type"/> object representing
		/// some private or otherwise complex type. It generates the expression tree for:
		/// 'System.Type.GetType("AssemblyQualifiedName", true)'.
		/// This looks horrible in code but works for about any type, including array types,
		/// generic types, private types, etc.
		/// </summary>
		/// <param name="info"></param>
		/// <returns></returns>
		public static CodeExpression CreateCodeToFetchPrivateType(Type info)
		{
			return new CodeMethodInvokeExpression(
				new CodeTypeReferenceExpression(
					typeof(System.Type)
				),
				"GetType",
				new CodePrimitiveExpression(info.AssemblyQualifiedName),
				new CodePrimitiveExpression(true)  // throwOnError
			);
		}

		public static CodeTypeDeclaration CreateTypeDeclaration(Type info)
		{
			var type = new CodeTypeDeclaration();

			type.Name = info.Name + Constants.AccessorSuffix;
			type.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			//type.TypeAttributes |= TypeAttributes.Sealed;
			type.IsClass = true;

			var privateObjectFieldType = info.IsEntirelyPublic() ? info : typeof(object);
			var privateObjectField = new CodeMemberField(privateObjectFieldType, Constants.PrivateObjectField);
			privateObjectField.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			type.Members.Add(privateObjectField);

			var privateTypeField = new CodeMemberField(typeof(Type), Constants.PrivateTypeField);
			privateTypeField.Attributes = MemberAttributes.Static | MemberAttributes.Public | MemberAttributes.Final;
			privateTypeField.InitExpression = CreateCodeToFetchPrivateType(info);
			type.Members.Add(privateTypeField);

			var specialConstructor = new CodeConstructor();
			specialConstructor.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			specialConstructor.Parameters.Add(
				new CodeParameterDeclarationExpression(privateObjectFieldType, Constants.PrivateObjectField));
			specialConstructor.Statements.Add(
				new CodeAssignStatement(
					CreatePrivateObjectReferenceExpression(),
					new CodeArgumentReferenceExpression(Constants.PrivateObjectField)
				)
			);
			type.Members.Add(specialConstructor);

			return type;
		}

		/// <summary>
		/// Creates an expression tree for 'PrivateType.GetProperty("Name", BindingFlags.NonPublic | BindingFlags.Instance)'
		/// or 'PrivateType.GetField("Name", BindingFlags.NonPublic | BindingFlags.Instance)'.<br/><br/>
		/// The name and BindingFlags vary depending on the attributes of info.
		/// </summary>
		/// <param name="info">Property / field metadata</param>
		/// <returns></returns>
		public static CodeExpression CreateGetPropertyOrFieldMethodInvokeExpression(MemberInfoAdapter info)
		{
			var getPropertyOrField = new CodeMethodInvokeExpression(
				CreatePrivateTypeReferenceExpression(),
				info.GetterMethod, // "GetProperty" or "GetField"
				new CodePrimitiveExpression(info.Name),
				CreateBindingFlagsReferenceExpression(info.IsPublicGet, info.IsStatic)
			);

			return getPropertyOrField;
		}

		/// <summary>
		/// Creates an expression for:
		/// <list type="bullet">
		/// <item>'PrivateType.GetProperty(...).GetValue(this.PrivateObject, null)'</item>
		/// <item>'PrivateType.GetField(...).GetValue(this.PrivateObject)'</item>
		/// <item>'PrivateType.GetProperty(...).SetValue(this.PrivateObject, value, null)'</item>
		/// <item>'PrivateType.GetField(...).SetValue(this.PrivateObject, value)'</item>
		/// </list>
		/// For static properties/fields, 'this.PrivateObject' is replaced by 'null'. See
		/// <see cref="CreateGetPropertyOrFieldMethodInvokeExpression"/> for details about the
		/// arguments passed to <see cref="Type.GetProperty"/> / <see cref="Type.GetField"/>.
		/// </summary>
		/// <param name="info">Property / field metadata</param>
		/// <param name="setValue">Create 'SetValue' variant (as opposed to 'GetValue')</param>
		/// <returns></returns>
		public static CodeExpression CreateGetOrSetValueMethodInvokeExpression(MemberInfoAdapter info, bool setValue)
		{
			var getValue = new CodeMethodInvokeExpression(
				CreateGetPropertyOrFieldMethodInvokeExpression(info),
				setValue ? "SetValue" : "GetValue",
				info.IsStatic ? new CodePrimitiveExpression(null) : CreatePrivateObjectReferenceExpression()
			);

			if (setValue)
			{
				// SetValue takes a value as extra argument
				if (info.ReturnType.IsPublic)
				{
					getValue.Parameters.Add(new CodePropertySetValueReferenceExpression());
				}
				else
				{
					getValue.Parameters.Add(new CodeFieldReferenceExpression(
						new CodePropertySetValueReferenceExpression(), Constants.PrivateObjectField));
				}
			}

			if (info.MemberInfo is PropertyInfo)
			{
				// PropertyInfo.GetValue takes an extra argument (for indexed properties)
				getValue.Parameters.Add(new CodePrimitiveExpression(null));
			}

			return getValue;
		}

		/// <summary>
		/// Create the statements for the 'get {}' block of an accessor property for a field/property.
		/// This is either simply 'return this.PrivateObject.PropertyOrFieldName;' for public members or a
		/// more complex expression as created with <see cref="CreateGetOrSetValueMethodInvokeExpression"/>.
		/// </summary>
		/// <param name="info">Property / field metadata</param>
		/// <returns></returns>
		public static CodeStatement[] CreateMemberPropertyGetStatements(MemberInfoAdapter info)
		{
			if (info.IsPublic && info.IsReflectedTypePublic)
			{
				return new CodeStatement[] {
					new CodeMethodReturnStatement(
						// TODO: emit CodeFieldReferenceExpression for fields
						new CodePropertyReferenceExpression(
							info.IsStatic
								? new CodeTypeReferenceExpression(info.ReflectedType)
								: CreatePrivateObjectReferenceExpression(),
							info.Name
						)
					)
				};
			}
			else
			{
				var getValue = CreateGetOrSetValueMethodInvokeExpression(info, false);

				return new CodeStatement[] {
					new CodeMethodReturnStatement(CreateCastExpression(info.ReturnType, getValue))
				};
			}
		}

		/// <summary>
		/// Create the statements for the 'set {}' block of an accessor property for a field/property.
		/// Similar to <see cref="CreateMemberPropertyGetStatements"/>.
		/// </summary>
		/// <param name="info">Property / field metadata</param>
		/// <returns></returns>
		public static CodeStatement[] CreateMemberPropertySetStatements(MemberInfoAdapter info)
		{
			if (info.IsPublic && info.IsReflectedTypePublic)
			{
				return new CodeStatement[] {
					new CodeAssignStatement(
						// TODO: emit CodeFieldReferenceExpression for fields
						new CodePropertyReferenceExpression(
							info.IsStatic
								? new CodeTypeReferenceExpression(info.ReflectedType)
								: CreatePrivateObjectReferenceExpression(),
							info.Name
						),
						new CodePropertySetValueReferenceExpression()
					)
				};
			}
			else
			{
				var setValue = CreateGetOrSetValueMethodInvokeExpression(info, true);

				return new CodeStatement[] {
					new CodeExpressionStatement(setValue)
				};
			}
		}

		/// <summary>
		/// Create a reference to a <see cref="Type"/>. If the type is private a reference to the
		/// matching accessor type is created instead.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		public static CodeTypeReference CreateTypeReference(Type type)
		{
			if (type.IsEntirelyPublic())
			{
				return new CodeTypeReference(type);
			}
			else if (type.IsArray)
			{
				// Array type with the element type non-public.

				// Similar workaround as for generic types (see below),
				// just return base class now instead of an interface.

				return new CodeTypeReference(typeof(Array));
			}
			else if (type.IsGenericType)
			{
				// Generic type with at least one non-public generic argument

				// As a simple workaround for the problem of generating a wrapper
				// of the generic type with the accessor as generic argument,
				// just return the most specialized interface implemented by the type.

				// This works nicely with e.g. collections and testing whether they
				// contain some particular element, because e.g. List<> has IList
				// as first non-generic interface, and IList is a reasonably rich
				// interface for testing purposes (enumerable, containment, etc.)

				// First filter out all not entirely public interfaces.
				var interfaces = type.GetInterfaces().Where(i => i.IsEntirelyPublic());

				// Find all interfaces for which there exists no interface that implements that interface.
				var interfaces2 = interfaces.Where(i => !interfaces.Any(j => j.GetInterfaces().Contains(i)));

				if (interfaces2.Count() > 0)
					return new CodeTypeReference(interfaces2.First());

				return new CodeTypeReference(typeof(object));
			}
			else if (type.IsGenericParameter)
			{
				// Generic parameter (ie. the Type in void 'MyGenericMethod<Type>(Type arg1, Type arg2)')

				return new CodeTypeReference(type.Name, CodeTypeReferenceOptions.GenericTypeParameter);
			}
			else
			{
				// Private non-generic type

				// Prefix for nested types.
				string prefix = "";
				for (Type t = type.DeclaringType; t != null; t = t.DeclaringType)
					prefix += t.Name + Constants.AccessorSuffix + ".";

				return new CodeTypeReference(type.Namespace + "." + prefix + type.Name + Constants.AccessorSuffix);
			}
		}

		/// <summary>
		/// Create an expression to convert the result of another expression to the given type.
		/// If the type is public this creates a cast, otherwise it creates code similar to:
		/// 'new PrivateTestClass_Accessor(right)'.
		/// </summary>
		/// <param name="type"></param>
		/// <param name="right"></param>
		/// <returns></returns>
		public static CodeExpression CreateCastExpression(Type type, CodeExpression right)
		{
			if (type.IsEntirelyPublic() || ((type.IsArray || type.IsGenericType || type.IsGenericParameter) && type.IsPublic))
			{
				return new CodeCastExpression(CreateTypeReference(type), right);
			}
			else
			{
				return new CodeObjectCreateExpression(CreateTypeReference(type), right);
			}
		}

		/// <summary>
		/// Creates an accessor property for a property.
		/// </summary>
		/// <param name="info">Property metadata</param>
		/// <returns></returns>
		public static CodeMemberProperty CreateMemberProperty(PropertyInfo info)
		{
			var property = new CodeMemberProperty();

			property.Name = info.Name;
			property.Type = CreateTypeReference(info.PropertyType);
			property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

			if (info.GetAccessors(true).Any((m) => (m.Attributes & MethodAttributes.Static) == MethodAttributes.Static))
			{
				property.Attributes |= MemberAttributes.Static;
			}

			if (info.CanRead)
			{
				property.GetStatements.AddRange(CreateMemberPropertyGetStatements(new MemberInfoAdapter(info, false)));
			}

			if (info.CanWrite)
			{
				property.SetStatements.AddRange(CreateMemberPropertySetStatements(new MemberInfoAdapter(info, true)));
			}

			return property;
		}

		/// <summary>
		/// Creates an accessor property for a field.
		/// </summary>
		/// <param name="info">Field metadata</param>
		/// <returns></returns>
		public static CodeMemberProperty CreateMemberProperty(FieldInfo info)
		{
			var property = new CodeMemberProperty();

			property.Name = info.Name;
			property.Type = CreateTypeReference(info.FieldType);
			property.Attributes = MemberAttributes.Public | MemberAttributes.Final;

			if (info.IsStatic)
			{
				property.Attributes |= MemberAttributes.Static;
			}

			property.GetStatements.AddRange(CreateMemberPropertyGetStatements(new MemberInfoAdapter(info)));

			if (!info.IsInitOnly && !info.IsLiteral) // C# "readonly" and "const" keywords
			{
				property.SetStatements.AddRange(CreateMemberPropertySetStatements(new MemberInfoAdapter(info)));
			}

			return property;
		}

		static void FillConstructorOrMethod(CodeMemberMethod method, MethodInfoAdapter info)
		{
			method.Name = info.Name;
			method.Attributes = MemberAttributes.Public | MemberAttributes.Final;
			method.Parameters.AddRange(info.ParameterDeclarationExpressionArray);

			if (info.MethodInfo.IsGenericMethod)
			{
				foreach (var param in info.MethodInfo.GetGenericArguments())
				{
					method.TypeParameters.Add(new CodeTypeParameter(param.Name));
				}
			}

			if (info.IsPublic && info.IsReflectedTypePublic)
			{
				if (info.MethodInfo is ConstructorInfo)
				{
					method.Statements.Add(
						new CodeAssignStatement(
							CreatePrivateObjectReferenceExpression(),
							new CodeObjectCreateExpression(info.ReflectedType, info.ArgumentReferenceExpressionArray)
						)
					);
				}
				else
				{
					var invoke = new CodeMethodInvokeExpression(
						info.IsStatic
							? new CodeTypeReferenceExpression(info.ReflectedType)
							: CreatePrivateObjectReferenceExpression(),
						info.Name,
						info.ArgumentReferenceExpressionArray
					);

					CodeExpression right = invoke;

					if (info.ReturnType != typeof(void))
					{
						right = CreateCastExpression(info.ReturnType, right);
						method.Statements.Add(new CodeMethodReturnStatement(right));
					}
					else
					{
						method.Statements.Add(right);
					}
				}
			}
			else
			{
				var paramTypeArray = new CodeArrayCreateExpression(typeof(Type), info.ArgumentTypeOfExpressionArray);
				var paramValueArray = new CodeArrayCreateExpression(typeof(object), info.ArgumentReferenceExpressionArray);

				//_privateObject = _privateType.GetConstructor(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, System.Type.DefaultBinder, System.Type.EmptyTypes, null).Invoke(null);
				//_privateObject = _privateType.GetConstructor(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, System.Type.DefaultBinder, new System.Type[] { typeof(TestClassLibrary.PublicConstructor) }, null).Invoke(new object[] { x });

				//PrivateType.GetMethod("GenericMethod2",System.Reflection.BindingFlags.NonPublic|System.Reflection.BindingFlags.Instance).MakeGenericMethod(new System.Type[]{typeof(Type)}).Invoke(this.PrivateObject,new object[] {arg})

				var getMethod = new CodeMethodInvokeExpression(
					CreatePrivateTypeReferenceExpression(),
					info.MethodInfo is ConstructorInfo ? "GetConstructor" : "GetMethod"
				);

				if (!(info.MethodInfo is ConstructorInfo))
				{
					getMethod.Parameters.Add(new CodePrimitiveExpression(info.Name));
				}

				getMethod.Parameters.Add(CreateBindingFlagsReferenceExpression(info.IsPublic, info.IsStatic));

				if (info.MethodInfo.IsGenericMethod)
				{
					// Generic methods require a different way of invoking:
					// First we get the generic method using GetMethod(Name, BindingFlags),
					// then we use MakeGenericMethod(new Type[]{type1,type2,...}) to actually fetch/make
					// the MethodInfo object on which we can invoke 'Invoke'.

					getMethod = new CodeMethodInvokeExpression(
						getMethod,
						"MakeGenericMethod",
						paramTypeArray
					);
				}
				else
				{
					getMethod.Parameters.Add(new CodePropertyReferenceExpression(
						new CodeTypeReferenceExpression(typeof(Type)), "DefaultBinder"));
					getMethod.Parameters.Add(paramTypeArray);
					getMethod.Parameters.Add(new CodePrimitiveExpression(null));
				}

				var invoke = new CodeMethodInvokeExpression(getMethod, "Invoke", paramValueArray);
				CodeExpression right = invoke;

				if (info.MethodInfo is ConstructorInfo)
				{
					if (info.IsReflectedTypePublic)
					{
						right = new CodeCastExpression(info.ReflectedType, right);
					}

					method.Statements.Add(
						new CodeAssignStatement(CreatePrivateObjectReferenceExpression(), right)
					);
				}
				else
				{
					invoke.Parameters.Insert(0,
						info.IsStatic ? new CodePrimitiveExpression(null) : CreatePrivateObjectReferenceExpression()
					);

					if (info.ReturnType != typeof(void))
					{
						right = CreateCastExpression(info.ReturnType, right);
						method.Statements.Add(new CodeMethodReturnStatement(right));
					}
					else
					{
						method.Statements.Add(right);
					}
				}
			}
		}

		public static CodeConstructor CreateConstructor(ConstructorInfo info)
		{
			var method = new CodeConstructor();

			FillConstructorOrMethod(method, new MethodInfoAdapter(info));

			return method;
		}

		public static CodeMemberMethod CreateMemberMethod(MethodInfo info)
		{
			var method = new CodeMemberMethod();

			method.ReturnType = CreateTypeReference(info.ReturnType);

			FillConstructorOrMethod(method, new MethodInfoAdapter(info));

			if (info.IsStatic)
			{
				method.Attributes |= MemberAttributes.Static;
			}

			return method;
		}
	}
}
